package br.unicamp.mc715.bank;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import java.util.Random;

import br.unicamp.mc715.bank.BankOperation.BankOperationRequest;
import br.unicamp.mc715.bank.BankOperation.BankOperationResponse;
import br.unicamp.mc715.common.MC715Exception;
import br.unicamp.mc715.common.MC715IOException;
import br.unicamp.mc715.common.MC715SerializationException;
import br.unicamp.mc715.transport.Message;
import br.unicamp.mc715.transport.Transport;
import br.unicamp.mc715.transport.TransportId;
import br.unicamp.mc715.transport.UDPTransport;
import br.unicamp.mc715.transport.UDPTransportId;

public abstract class BankClient implements Runnable {
	/** Número de contas deste banco*/
	protected int numberOfAccounts;
	
	/** Id do servidor que irá receber as requisições */
	protected TransportId serverId;
	
	/** Transporte utilizado para comunicar com {@link #serverId} */
	Transport transport;
	
	/**
	 * Instancia um banco com <code>numberOfAccounts</code> contas, e que se comunica 
	 * com o servidor em <code>serverId</code> para executar as operações.
	 * 
	 * @param serverId O servidor ao qual iremos fazer as requisições das operações.
	 * @param numberOfAccounts O número de contas cadastradas no banco. 
	 */
	public BankClient(TransportId serverId, int numberOfAccounts) throws MC715IOException{
		this.numberOfAccounts = numberOfAccounts;
		this.serverId = serverId;
		this.transport = new UDPTransport();
		
		System.out.println("Cliente rodando em " + transport.getId());
	}
	
	/**
	 * Retorna o tipo do banco, isto é, "normal" para um banco iterativo ({@link NormalBank}) e 
	 * "auto" para um banco que executa operações aleatórias ({@link AutoBank}).
	 * @return "normal" ou "auto"
	 */
	public abstract String getBankType();
	
	/**
	 * Requisita ao cluster do Banco a execução da operação especificada
	 * @param op Operação requisitada
	 * @throws MC715SerializationException 
	 * @throws MC715IOException 
	 */
	protected void addOperation(BankOperation operation) throws MC715IOException, MC715SerializationException
	{
		//Envia a requisição. Não espera nenhuma resposta.
		if (operation.getOperationAccount() > 0)
		{
			BankOperationRequest request = new BankOperationRequest(operation);
			transport.sendMessage(request, serverId);
		}
	}
	
	/**
	 * Requisita ao cluster do Banco a execução da operação especificada
	 * @param op Operação requisitada
	 * @param out Terminal de saida
	 * @throws MC715SerializationException 
	 * @throws MC715IOException 
	 */
	protected void addOperation(BankOperation operation, PrintStream out) throws MC715IOException, MC715SerializationException
	{
		//Envia a requisição. Não espera nenhuma resposta.
		if (operation.getOperationAccount() >= 0 && operation.getOperationAccount()<numberOfAccounts)
		{
			BankOperationRequest request = new BankOperationRequest(operation);
			transport.sendMessage(request, serverId);
		}
		else
		{
			out.println("Conta invalida");
		}
	}

	/**
	 * Requisita ao cluster do Banco a execução da operação de "Verificar Saldo"
	 * @param operation Operação de "verificar saldo"
	 * @throws MC715SerializationException 
	 * @throws MC715IOException 
	 */
	protected int getBalance(BankOperation operation, PrintStream out) throws MC715IOException, MC715SerializationException
	{
		//Envia a requisição
		if (operation.getOperationAccount() >= 0 && operation.getOperationAccount()<numberOfAccounts)
		{
			BankOperationRequest request = new BankOperationRequest(operation);
			transport.sendMessage(request, serverId);
			//Espera uma mensagem de resposta.
			//Como só esperamos uma resposta por vez, não tem problema descartar mensagens até encontrar a esperada.
			while (true) {
				Message msg = transport.receiveMessage();
				if (msg.getPayload() instanceof BankOperationResponse) {
					BankOperationResponse response = (BankOperationResponse)msg.getPayload();
					if (response.operation.getClock().getId().equals(msg.getSender()))
					{
						return response.result;
					}
				}
			}
		}
		else
		{
			out.println("Conta invalida");
			return 0;
		}
	}
	
	
	/** Um BankClient não-iterativo que executa {@link BankClient#numberOfAccounts} * {@link AutoBank#numberOfOperations} operações de saque e depósito aleatórias. */
	public static class AutoBank extends BankClient {
		/** Número de operações executadas pelo "auto-bank"*/
		private final int numberOfOperations;
		
		/**
		 * Instancia um banco automático com <code>numberOfAccounts</code> contas, que se comunica 
		 * com o servidor em <code>serverId</code> para executar as operações, e que irá executar 
		 * aproximadamente <code>numberOfOperations</code> operações em cada conta.
		 * 
		 * @param serverId O servidor ao qual iremos fazer as requisições das operações.
		 * @param numberOfAccounts O número de contas cadastradas no banco.
		 * @param numberOfOperations Número médio de operações a ser executado em cada conta  
		 * @throws MC715IOException Se for impossível iniciar o Transporte com o servidor
		 */
		public AutoBank(TransportId serverId, int numberOfAccounts, int numberOfOperations) throws MC715IOException {
			super(serverId, numberOfAccounts);
			this.numberOfOperations = numberOfOperations;
		}
		
		/**Executa {@link BankClient#numberOfAccounts} * {@link #numberOfOperations} operações de saque e depósito aleatórias.*/
		public void run() {
			/* Get random numbers */
			Random bankRandom = new Random();
			
			for (int i = 0; i < numberOfAccounts; i++) {
				for (int j = 0; j < numberOfOperations; j++) {
					try {
						Thread.sleep(1);
					} catch (InterruptedException e) {}

					try {
						if (bankRandom.nextBoolean()) 
							addOperation(new BankOperation('d', bankRandom.nextInt(numberOfOperations), i, transport.getId()));
						else
							addOperation(new BankOperation('w', bankRandom.nextInt(numberOfOperations), i, transport.getId()));
					} catch (MC715Exception e) {
						e.printStackTrace();
					}
				} /* for j */			
			} /* for i */

			System.exit(0);
		}
		
		/**Retorna "auto"*/
		@Override
		public String getBankType() {
			return "auto";
		}
	}

	/** Um BankClient iterativo que executa comandos digitados no terminal */
	public static class NormalBank extends BankClient {
		/**Entrada do usuário*/
		BufferedReader terminalIn;
		/**Saída do programa*/
		PrintStream terminalOut;
		
		/**
		 * Instancia um banco iterativo com <code>numberOfAccounts</code> contas e que se comunica 
		 * com o servidor em <code>serverId</code> para executar as operações.
		 * 
		 * @param serverId O servidor ao qual iremos fazer as requisições das operações.
		 * @param numberOfAccounts O número de contas cadastradas no banco.
		 * @param userInput Entrada do usuário.
		 * @throws MC715IOException Se for impossível iniciar o Transporte com o servidor
		 */
		public NormalBank(TransportId serverId, int numberOfAccounts, Reader userInput, 
				PrintStream userOutput) throws MC715IOException {
			super(serverId, numberOfAccounts);
			this.terminalIn = new BufferedReader(userInput);
			this.terminalOut = userOutput;
		}
		
		/**Inicia uma iteração com o usuário, lendo as operações desejadas de {@link #terminal}*/
		public void run() {
			while(true)  {
				try {
		            System.out.print("> ");
					String operation = terminalIn.readLine();

	                if (operation == null) break;
	                String[] operationSlices = operation.split("\\s+");
	                if (operationSlices.length == 0) continue; //Operação vazia
	                
	                //Lê os parâmetros: Número da conta e Valor
	                int account = -1;
	                int value = -1;
	                
	                if (! "c".equals(operationSlices[0]) && ! "h".equals(operationSlices[0]))
	                {
	                	if (operationSlices.length >= 2)
	                		account = Integer.parseInt(operationSlices[1]);
	                	if (operationSlices.length >= 3)
	                		value = Integer.parseInt(operationSlices[2]);
	                }
	                
	                //Trata todas operações possíveis
	                if ("q".equals(operationSlices[0]) && operationSlices.length >= 1) {
	                    break;
	                    
	                } else if ("c".equals(operationSlices[0]) && operationSlices.length > 1) {
	                	// Conecta a um outro banco
	                	serverId = new UDPTransportId(operationSlices[1]);
	                	terminalOut.println("Conectado a "+ operationSlices[1]);
	                
	                } else if ("h".equals(operationSlices[0])) {
	                	// Mostra ajuda
	                	terminalOut.println("b A   - Get balance of account \nd A V - Deposit value V to account A\n"+
	                			"w A V - Withdraw value V from account A\ni     - Get balance info on all accounts");
	                
	                } else if ("b".equals(operationSlices[0]) && operationSlices.length >= 2) {
	                	terminalOut.println("Balance: " + 
	                			getBalance(new BankOperation('b', account, transport.getId()), terminalOut));

	                } else if ("d".equals(operationSlices[0]) && operationSlices.length >= 3) {
	                	addOperation(new BankOperation('d', value, account, transport.getId()), terminalOut);
	                	terminalOut.println("Deposit Requested");

	                } else if ("w".equals(operationSlices[0]) && operationSlices.length >= 3) {
	                	addOperation(new BankOperation('w', value, account, transport.getId()), terminalOut);
	                	terminalOut.println("Withdraw Requested");

	                } else if ("i".equals(operationSlices[0]) && operationSlices.length >= 1) {
						for (int i = 0; i < numberOfAccounts; i++) {
							terminalOut.println("Account #" + i + " - Balance: " + 
									getBalance(new BankOperation('b', i, transport.getId()), terminalOut));
						}
	                }
				} catch (MC715Exception e) {
					e.printStackTrace(terminalOut);
				} catch (IOException e) {
					break; //Perdeu a conexão com o usuário.
				} 
			}
		}
		
		/**Retorna "normal"*/
		@Override
		public String getBankType() {
			return "normal";
		}
	}
	
	/** Método principal, cria e executa um cliente de Banco.
	 * @param args Argumentos de linha de comando
	 * @throws MC715Exception Se ocorrer algum erro
	 * @throws NumberFormatException Se ocorrer algum erro
	 * @throws IOException Se ocorrer algum erro
	 */
	public static void main(String[] args) throws MC715Exception, NumberFormatException, IOException 
	{
		if (args.length != 3 || (!"normal".equals(args[0]) && !"auto".equals(args[0]))) 
		{
			System.out.println("Usage: java -jar mc715client.jar normal|auto <number of accounts> <server id>");
			System.out.println("Usage: <server id> is the UDP address of one of the servers.");
			System.exit(0);
		}

		if ("normal".equals(args[0])) {
			new NormalBank(new UDPTransportId(args[2]), Integer.parseInt(args[1]), new InputStreamReader(System.in), System.out).run();
		} else {
			new AutoBank(new UDPTransportId(args[2]), Integer.parseInt(args[1]), 100).run();
		}
		
	}
}
